#!/usr/bin/env python

#   Trolls Outta Luckland v0.0.6
#   Copyright 2009-2010, Gummbum
#
#   This file is part of Trolls Outta Luckland.
#
#   Trolls Outta Luckland is free software: you can redistribute it
#   and/or modify it under the terms of the GNU General Public License
#   as published by the Free Software Foundation, either version 3 of
#   the License, or (at your option) any later version.
#
#   Trolls Outta Luckland is distributed in the hope that it will be
#   useful, but WITHOUT ANY WARRANTY; without even the implied warranty
#   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with Trolls Outta Luckland.  If not, see
#   <http://www.gnu.org/licenses/>.

"""
To do:
    v0.0.7:
    *   Need a debug hud.
    *   Make shuffle a little smarter. Don't want the song I just heard to be
        next.
    *   SpritStripAnim should handle iterable rect and count.
    *   Bug? sprite.LayeredUpdates does not seem to be drawing sprites in the
        expected layer order. Enemy shots are on top of enemies.
    *   New enemy classes.
    *   Fancier (harder to ace) convoy patterns. Who wants to ace these,
        anyway? The points suck.
    *   More interesting and obvious ramming techniques.
    *   Continue graphic and audio additions.
    v0.0.8:
    *   Defensive powerups.
    *   Offensive powerups.
    *   New weapons.
    v1.0.0:
    *   Tweak gameplay.
Done:
    v0.0.7:
    *   Added psyco support.
    v0.0.6:
    *   Better "Loading..." screen.
    *   Load an enemy of each kind in Game.run to generate the hitmasks.
    *   Got rid of huge border when in windowed display.
    *   Reduced height requirement so game will fit on 1024x768 monitors.
    *   Tweaked factory: implemented per-type max_on_patrol instead of the
        shared max_on_patrol. Whilst in there, the patrol dispatcher was
        enhanced to turn out more interesting combinations.
    *   Tweaked the algorithm that increases max-on-patrol over time.
        *   For each game, every five waves the default for all enemy types
        increases by 1. Net effect: every five waves the wave starts out a
        bit harder.
        *   For each wave, about every 30 seconds a random enemy type's max
            will be increased by 1. This is cumulative for the wave only. It
            is reset to the game default when the next wave starts. Net
            effect: every 30 seconds the wave gets a bit harder.
    *   Fixed: Menu and marquis sounds were playing over music. They no
        longer play if a song is playing.
    v0.0.5:
    *   Fixed "ImportError: Bad magic number in lib/game_globals.pyc".
    v0.0.4:
    *   Added rotating images to sprites.
    *   Changed player ship steering dynamics.
    *   Mouse homes to ship at level start; homes to saved position if
        gameplay is interrupted (escape key).
    *   Windowed and fullscreen are choices now. Set in game_globals.py. Use
        F12 to toggle.
    *   Game speed derives from measured FPS. Classes' internal speed
        compensates.
    *   Enlarged SPACE*.png and Explode*.png sprite strips. <3 GIMP!
    *   New look for laser shots.
    *   Baddies fire directly at player if facing player.
    *   Sprite strip loader creates arbitrary number of rotated images
        and hitmasks. Hitmask generation very slow! Caching optimization
        implemented. Still slow for the many images needed, but works.
    *   New sign-on UI uses keyboard instead of mouse. Remembers last
        player. Added player picker, which selects from high score holders.
    *   Fixed: Stage stat updated immediately after killing last baddy.
        It now updates when next the level's herald posts.
    *   Fixed: Enemies warp when returning to standby formation.
    *   F12 toggles fullscreen.
    *   Mood music for Game._play(). Keys for: change track, mute, music
        volume, FX volume, and master volume. Settings are saved.
    *   EnemyFactory._make_fleet: optimized generation of enemy paths. Level
        loading speeded up greatly.
    *   SpriteSheetAnim performance. The game requires a lot of sprites, and
        a lot of hitmasks. Hitmask generation is extremely expensive. Ship
        needs 3 images * 360 rotations = 1080. Each Enemy needs
        3 images * 90 = 270. Just two Enemy subclasses takes over 5 seconds
        to gen hitmasks, a big ugly hitch in gameplay. Did the following to
        minimize that:
        *   Pre-generate hitmasks as Python modules. Compile to .pyc. Ship
            takes 2 GB memory to compile, but it loads very fast.
        *   Ship construction moved up to program load.
    v0.0.3:
    *   run.py is snowballing. Moved some classes out to window.py,
        high_scores.py, player.py, and ui.py.
    *   Graphic FX added.
    *   Sound FX added.
    v0.0.2:
    *   Path points optimized to reduce calculate_bezier() times. Steps
        reduced from 300 to 200: much faster level generation.
    *   Image cache.
    *   Much UI.
    *   Enemies shoot back.
    *   Integrated game loop states with intuitive menu.
    *   Dashboard: ship counter, score, stage, player name.
    *   Player name input.
    *   High scores.
    v0.0.1:
    *   Window, ship, pointer, and swerving enemies to shoot.
"""

__version__ = '0.0.4'
__revision__ = '0.0.7'

## Python imports
import os
from os.path import join as join_path
import sys
import random
from random import randrange as rand
import time

sys.path.append('lib')

try:
    ## Pygame imports
    import pygame
    from pygame.locals import (
        Color,
        QUIT, KEYDOWN, KEYUP, MOUSEBUTTONDOWN, MOUSEBUTTONUP,
        K_ESCAPE, K_BACKSPACE, K_RETURN, K_UP, K_DOWN, K_F12,
        K_MINUS, K_UNDERSCORE, K_PLUS, K_EQUALS,
        K_LEFTBRACKET, K_RIGHTBRACKET,
        K_PERIOD, K_COMMA, K_LESS, K_GREATER,
        K_0, K_1, K_2, K_3, K_4, K_5, K_6, K_7, K_8, K_9,
        K_d, K_g, K_h, K_i, K_l, K_m, K_p, K_s, K_t, K_v, K_w,
    )
    ## Game library imports
    import game_globals
    from game_globals import *
    import enemy
    from enemy import EnemyFactory, Enemy
    from high_scores import SaveScore, HighScores
    from fps import Fps
    import gumm_sound
    from pixelperfect import *
    from player import Ship
    import settings
    from sky import Sky
    from utils import (
        calc_angle, draw_text, get_font, load_image, mouse_to_game,
        game_to_mouse,
    )
    from ui import (
        color as ui_color,
        layout as ui_layout,
        UILabel, Herald, PlayerName, ShipCounter,
        Score, NowPlaying, Stage, Stat, Statf, Pointer,
    )
    from window import Window
except ImportError, e:
    print 'ImportError: %s' % e
    print '    in file "%s"' % os.path.abspath(sys.argv[0])

game = None
jukebox = None

class MusicTracks(list):
    def __init__(self):
        self.iter()
    def iter(self):
        self.i = 0
        return self
    def next(self):
        if self.i >= len(self):
            self.i = 0
        item = self[self.i]
        self.i += 1
        return item
    def shuffle(self):
        t = list(self)
        del self[:]
        while len(t):
            i = rand(len(t))
            self.append(t.pop(i))
music_tracks = MusicTracks()

class SpriteGroups():
    def __init__(self):
        self.all = pygame.sprite.LayeredUpdates()
        self.players = pygame.sprite.Group()
        self.enemies = pygame.sprite.Group()
        self.player_shots = pygame.sprite.Group()
        self.enemy_shots = pygame.sprite.Group()
        self.powerups = pygame.sprite.Group()
        self.pointer = pygame.sprite.Group()
        self._groups = {
            1 : self.pointer,
            4 : self.player_shots,
            7 : self.enemy_shots,
            10 : self.players,
            13 : self.enemies,
            16 : self.powerups,
        }
        # Layers for self.all = LayeredUpdates()...
        # Obscured effects go in layer-1.
        # Obscuring effects go in layer+1.
        self.layers = {
            'pointer' : 1,
            'player_shots' : 4,
            'enemy_shots' : 7,
            'players' : 10,
            'enemies' : 13,
            'powerups' : 16,
        }
    def empty(self):
        for g in (self.player_shots, self.enemy_shots, self.enemies,
                    self.powerups):
            for s in g.sprites():
                s.kill()
    def add(self, *sprites, **kwargs):
        """kwargs: layer='layer', over=False, under=False"""
        layer = kwargs.get('layer', None)
        over = kwargs.get('over', False)
        under = kwargs.get('under', False)
        l = self.layers[layer]
        if over is True:
            l += 1
        elif under is True:
            l -= 1
        self.all.add(layer=l, *sprites)
        if over or under:
            pass
        else:
            self._groups[l].add(*sprites)
    def collide_pve(self):
        check1 = self._groupcollide(self.players, self.enemies)
        check2 = self._groupcollide(self.enemies, self.player_shots)
        check3 = self._groupcollide(self.players, self.enemy_shots)
        for hits in (check1,check2,check3):
            for s in hits.keys():
                s.hit(hits[s])
    def collide_powerups(self):
        check1 = self._groupcollide(self.players, self.powerups)
        check2 = self._groupcollide(self.enemies, self.powerups)
        for hits in (check1,check2):
            for s in hits.keys():
                s.power_up(hits[s])
    def _groupcollide(self, groupa, groupb):
        hits = {}
        for s1 in groupa.sprites():
            for s2 in groupb.sprites():
                if check_collision(s1, s2):
                    if s1 not in hits:
                        hits[s1] = []
                    hits[s1].append(s2)
        return hits

class Game(object):
    def __init__(self, name=None):
        self._name = settings.name
        self.state = POST_MENU
        self.prev_state = NEW
        self.in_progress = False
        self.avg_fps = 0
        self.game_speed =1 
        self.save_mouse = pygame.mouse.get_pos()
        #
        self.window = Window()
        self.groups = None
        self.menu = None
        self.fps = None
        self.pointer = None
        self.sky = None
        self.player = None
        self.enemy_factory = None
        self.stats = None
        self.hud = None
        self.debugs = None
        #
        self.name = None
        self.stage = None
        self.score = None
        self.ship_counter = None
        self.now_playing = None
        self.high_scores = None
        #
        self.name_label = None
        self.score_label = None
        self.stage_label = None
        self.ships_label = None
        #
        self.fx_volume = settings.fx_volume
        self.music_volume = settings.music_volume
        self.music_paused = False
        self.sound = None
        self.channel = None
        self.show_hud = True
        self.show_debugs = True
        self._invulnerable = False
        self._grab_mouse = True
        self.pause = False
    def run(self):
        self.fps = Fps(self.window._surface, ui_layout().fps)
        self.avg_fps = self.fps.benchmark()
        if FPS is not 0:
            self.max_fps = FPS
        else:
            self.max_fps = int(self.avg_fps * 0.9)
            if self.max_fps < 60:
                self.max_fps = 60
            elif self.max_fps > 120:
                self.max_fps = 120
        self.game_speed = 120.0 / self.max_fps
        #print 'Benchmark: AvgFPS=%.2f; Selected MaxFPS=%.2f GameSpeed=%.2f' % (
        #    self.avg_fps, self.max_fps, self.game_speed)
        #
        self.groups = SpriteGroups()
        self.menu = pygame.sprite.Group()
        self.pointer = Pointer()
        self.groups.add(self.pointer, layer='pointer')
        #
        self.sky = Sky()
        self.update_bg(stats=False)
        self.draw_bg(stats=False)
        h1 = Herald('Loading graphics...')
        h2 = Herald('(see readme.txt for user manual)', 2)
        h3 = Herald('(see license.txt for license and credits)', 4)
        [self.window.surface.blit(h.image, h.rect) for h in (h1,h2,h3)]
        self.window.flip()
        self.player = Ship()
        self.high_scores = HighScores()
        [UnitClass((0,0),(0,0)) for UnitClass in enemy._UNIT_TYPES.values()]
        # new game attributes, ship, and enemies
        self.stats = pygame.sprite.Group()
        self.score = Score()
        self.ship_counter = ShipCounter()
        self.stage = Stage()
        self.now_playing = NowPlaying()
        self.stats.add(self.fps, self.score, self.ship_counter, self.stage,
            self.now_playing)
        self._make_hud()
        self._make_debugs()
        #
        h1 = Herald('Loading sound...')
        self.update_bg(stats=False)
        self.draw_bg(stats=False)
        [self.window.surface.blit(h.image, h.rect) for h in (h1,h2,h3)]
        self.window.flip()
        jukebox.load_sound(join_path('sound','46485__PhreaKsAccount__comms3.ogg'),
            'computer greets', volume=0.6, sound_type=SOUND_FX)
        jukebox.load_ascii_file(join_path('sound','erhboom1.dat'),
            'sign on', volume=0.8, sound_type=SOUND_FX)
        jukebox.load_ascii_file(join_path('sound','dtdspatialharmony.dat'),
            'high scores', volume=0.8, sound_type=SOUND_FX)
        jukebox.load_ascii_file(join_path('sound','pencap.dat'),
            'click', volume=1, sound_type=SOUND_FX)
        jukebox.load_ascii_file(join_path('sound','scareself.dat'),
            'wait input', volume=0.7, sound_type=SOUND_FX)
        jukebox.load_sound(join_path('sound','power_up_16893.ogg'),
            'power up', volume=1, sound_type=SOUND_FX)
        jukebox['marquis'] = jukebox['sign on']
        n = len(music_tracks)
        i = 0
        name = None
        for i in range(n):
            h1 = Herald('Loading songs %d/%d...'%(i+1,n))
            self.update_bg(stats=False)
            self.draw_bg(stats=False)
            [self.window.surface.blit(h.image, h.rect) for h in (h1,h2,h3)]
            self.window.flip()
            name = music_tracks[i]
            jukebox.load_sound(join_path('music',name),
                name, volume=1, sound_type=SOUND_MUSIC)
        jukebox.set_type_volume(self.fx_volume, SOUND_FX)
        jukebox.set_type_volume(self.music_volume, SOUND_MUSIC)
        del i,h1,h2,h3,n,name
        #
        self._sign_on()
        if not CODER_PASS:
            self._post_high_scores()
        self.menu_click = None
        while 1:
            self._handle_events()
            state = self.state
            if state == PLAY:
                self._play()
            elif state == GET_MENU:
                self._get_menu()
            elif state == POST_MENU:
                self._post_menu()
            elif state in (FIRST,NEW):
                self._new()
            elif state == END:
                self._end()
            self.fps.tick(self.max_fps)
            self.window.flip()
    def set_state(self, state):
        if state in (PLAY,END):
            self.prev_state = state
        self.state = state
    def adjust_fx_volume(self, adjust):
        self.fx_volume = jukebox.adjust_type_volume(adjust, SOUND_FX)
    def adjust_music_volume(self, adjust):
        self.music_volume = jukebox.adjust_type_volume(adjust, SOUND_MUSIC)
    def adjust_master_volume(self, adjust):
        jukebox.adjust_master_volume(adjust)
    def play_song(self, interrupt=False):
        if len(music_tracks) == 0:
            return
        if interrupt and self.channel is not None:
            self.sound.fadeout()
        if not self.song_is_playing():
            self.sound = jukebox[music_tracks.next()]
            self.channel = self.sound.play()
            self.now_playing.set_text(self.sound.name)
    def song_is_playing(self):
        return self.channel is not None and \
                self.channel.get_busy() and \
                self.channel.get_sound() is self.sound
    def play_fx(self, name, mutex=False):
        if mutex:
            sound = jukebox[name]
            sound.play_mutex()
        else:
            sound = jukebox[name]
            sound.play()
    def update_bg(self, groups=[], all=False, standby=None, stats=True):
        self.window.clear()
        self.sky.update()
        if all:
            self.groups.all.update()
        else:
            self.groups.pointer.update()
        if self.enemy_factory is not None and standby is not None:
                self.enemy_factory.update(standby_only=standby)
        for obj in groups:
            obj.update()
        if stats:
            self._update_hud()
            self.stats.update()
        self.menu.update()
    def draw_bg(self, groups=[], all=False, stats=True):
        surface = self.window.surface
        self.sky.draw(surface)
        if all:
            self.groups.all.draw(surface)
        else:
            self.groups.pointer.draw(surface)
        for obj in groups:
            obj.draw(surface)
        if stats:
            self.stats.draw(self.window.surface)
        if self.state == GET_MENU:
            self.menu.draw(surface)
    def speed(self):
        return self.game_speed
    def quit(self):
        if self.high_scores is not None:
            self.high_scores.put(
                SaveScore(self.name.text, self.stage.value+1,
                    self.score.value))
        settings.save()
        pygame.display.quit()
        sys.exit()
    def _play(self):
        self.play_song()
        # do player vs. environment
        window = self.window
        player = self.player
        groups = self.groups
        self.window.clear()
        self.sky.update()
        self.sky.draw(self.window.surface)
        if not self._invulnerable:
            groups.collide_pve()
        groups.collide_powerups()
        if len(groups.players.sprites()) == 0:
            self.ship_counter.lose()
            self.set_state(END)
        if self.enemy_factory.is_done() and \
        len(groups.enemies.sprites()) == 0:
            self.set_state(END)
        groups.all.update(window.rect)
        if player.state & SHOOTING:
            shot = player.weapon.fire()
            if shot is not None:
                groups.player_shots.add(shot)
        self.enemy_factory.update()
        self.stats.update()
        self._update_hud()
        self._debug_it()
        groups.all.draw(window.surface)
        self.stats.draw(window.surface)
    def _new(self):
        self.menu_click = None
        self.high_scores.put(
            SaveScore(self.name.text, self.stage.value+1,
                self.score.value))
        if self.in_progress:
            self._post_high_scores()
        self.stage.reset()
        self.score.reset()
        self.ship_counter.reset()
        self.groups.empty()
        # do ready-set-go theatrics
        self._kill_heralds()
        rect = self.window.rect
        self.pointer.go_home()
        self.player.reset()
        self.groups.add(self.player, layer='players')
        self.enemy_factory = EnemyFactory(self.stage.value)
        self.ship_counter.reset()
        self.set_state(PLAY)
        self.in_progress = True
        self._marquis()
    def _post_menu(self):
        if self.in_progress:
            self.menu.add(Herald('Resume Game', 0))
        self.menu.add(Herald('New Game', 1),
            Herald('High Scores', 2), Herald('AWOL', 3))
        self.set_state(GET_MENU)
        pygame.event.get()
    def _get_menu(self):
        if not self.song_is_playing:
            self.play_fx('wait input', True)
        if self.menu_click is not None:
            for s in self.menu.sprites():
                if s.rect.collidepoint(mouse_to_game(self.menu_click)):
                    self.play_fx('click')
                    if s.text == 'Resume Game':
                        self.menu.empty()
                        self._marquis()
                        if self.in_progress:
                            self.pointer.set_real_pos(self.save_mouse)
                        self.state = self.prev_state
                    elif s.text == 'New Game':
                        self.menu.empty()
                        self.set_state(NEW)
                    elif s.text == 'High Scores':
                        jukebox['wait input'].fadeout()
                        self._post_high_scores()
                    elif s.text == 'AWOL':
                        self.quit()
        else:
            for s in self.menu.sprites():
                if s.rect.collidepoint(self.pointer.get_pos()):
                    if s.pulse is False:
                        s.set_pulse(True)
                elif s.pulse is True:
                    s.set_pulse(False)
        self.menu_click = None
        if self.in_progress:
            self.update_bg()
        else:
            self.update_bg(all=True, standby=True)
        self.draw_bg(all=True)
        if self.state != GET_MENU and not self.in_progress:
            jukebox['wait input'].fadeout()
    def _next_level(self):
        self.high_scores.put(
            SaveScore(self.name.text, self.stage.value+1, self.score.value))
        for s in self.groups.all.sprites():
            if type(s) not in (Pointer,Ship):
                s.kill()
        self.player.reset()
        self.stage.add()
        self.enemy_factory = EnemyFactory(self.stage.value)
        self._marquis()
        self.pointer.go_home()
    def _end(self):
        factory = self.enemy_factory
        anims = False
        for s in self.groups.all.sprites():
            isanim = True
            if issubclass(type(s), Enemy):
                if s.angle in (0,360):
                    isanim = False
                else:
                    isanim = True
            elif issubclass(type(s), Pointer) or issubclass(type(s), Ship):
                isanim = False
            if isanim:
                anims = True
                break
        if anims or not factory.is_done():
            # pass until anims and/or factory finish
            self._handle_events()
            self.update_bg(all=True, standby=True)
            self.draw_bg(all=True)
        elif self.player.alive():
            # player vanquished the wave
            self._next_level()
            self.set_state(PLAY)
        elif self.ship_counter.lives <= 0:
            # wave vanquished player; player ran out of ships
            self.in_progress = False
            self.set_state(POST_MENU)
            self.high_scores.put(
                SaveScore(self.name.text, self.stage.value+1, self.score.value))
            self._post_high_scores()
        else:
            # wave vanquished player; player has more ships
            self.ship_counter.update()
            self.player.reset()
            self.groups.add(self.player, layer='players')
            self.set_state(PLAY)
            if len(self.groups.enemies.sprites()) == 0:
                self._next_level()
            else:
                self.pointer.go_home()
                self._marquis()
    def _kill_heralds(self):
        for s in self.stats.sprites():
            if isinstance(s, Herald):
                s.kill()
    def _do_settings(self, key):
        if key in range(K_0,K_9):
            if DEBUG:
                self.enemy_factory._toggle_draw_path(e.key)
        elif key == K_d:
            self._toggle_debugs()
        elif key == K_g:
            self._grab_mouse ^= True
        elif key == K_h:
            self._toggle_hud()
        elif key == K_i:
            self._invulnerable ^= True
        elif key == K_l:
            if DEBUG:
                self._next_level()
        elif key == K_m:
            if self.sound is not None:
                if self.music_paused:
                    self.channel.unpause()
                    self.music_paused = False
                else:
                    self.channel.pause()
                    self.music_paused = True
        elif key == K_s:
            music_tracks.shuffle()
        elif key == K_t:
            if self.sound is not None:
                self.play_song(interrupt=True)
        elif key == K_v:
            self.pointer.visible ^= True
        elif key == K_w:
            self.player.revolve_turn_mode()
        elif key == K_RIGHTBRACKET:
            self.adjust_fx_volume(0.05)
        elif key == K_LEFTBRACKET:
            self.adjust_fx_volume(-0.05)
        elif key == K_MINUS:
            self.adjust_music_volume(-0.05)
        elif key in (K_PLUS,K_EQUALS):
            self.adjust_music_volume(0.05)
        elif key in (K_COMMA,K_LESS):
            self.adjust_master_volume(-0.05)
        elif key in (K_PERIOD,K_GREATER):
            self.adjust_master_volume(0.05)
    def _handle_events(self):
        for e in pygame.event.get():
            if e.type == KEYUP and e.key == K_F12:
                self.window.toggle_fullscreen()
            elif self.state == PLAY:
                if e.type == MOUSEBUTTONDOWN:
                    if e.button == 1:
                        if self.state == PLAY:
                            self.player.state |= SHOOTING
                elif e.type == MOUSEBUTTONUP:
                    if e.button == 1:
                        self.player.state &= ~SHOOTING
                        self.player.weapon.input_sync = 0
                        self.player.weapon.fire_rate = 0
                elif e.type == KEYUP:
                    if e.key == K_ESCAPE:
                        if self.prev_state not in (POST_MENU,GET_MENU):
                            self.save_mouse = self.pointer.get_real_pos()
                            self.prev_state = self.state
                            self.set_state(POST_MENU)
                    elif e.key == K_p:
                        self.pause ^= True
                    else:
                        self._do_settings(e.key)
            elif self.state in (POST_MENU,GET_MENU):
                if e.type == MOUSEBUTTONDOWN:
                    self.menu_click = e.pos
                elif e.type == KEYUP:
                    self._do_settings(e.key)
            elif e.type == KEYUP:
                if e.key == K_ESCAPE:
                    if self.prev_state not in (POST_MENU,GET_MENU):
                        self.save_mouse = self.pointer.get_real_pos()
                        self.prev_state = self.state
                        self.set_state(POST_MENU)
                elif e.key == K_p:
                    self.pause ^= True
                else:
                    self._do_settings(e.key)
    def _wait_mouse_up(self):
        pygame.event.get()
        if pygame.mouse.get_pressed()[0] == True:
            while 1:
                if pygame.event.wait().type == MOUSEBUTTONUP:
                    break
        pygame.event.get()
    def _segue(self, duration, spacing, *messages):
        """Display one or more transition messages, a la "Please wait..."
        duration is the number of seconds to pause
        spacing spreads out the vertical spacing of messages (try 1 or 2)
        messages are the strings to display
        """
        group = pygame.sprite.Group()
        window = self.window
        for i in range(len(messages)):
            group.add(Herald(messages[i], i*spacing))
        elapsed = 0
        duration *= 1000
        while elapsed < duration:
            self.window.clear()
            self.sky.update()
            self.stats.update()
            group.update()
            self.sky.draw(window.surface)
            group.draw(window.surface)
            if self.in_progress:
                self.stats.update(window._surface)
            self.window.flip()
            self.fps.tick(self.max_fps)
            elapsed += self.fps.get_time()
            for e in pygame.event.get():
                if e.type == KEYDOWN:
                    self._do_settings(e.key)
    def _marquis(self):
        window = self.window
        pointer = self.pointer
        group = self.groups.all
        stats = self.stats
        stage = 'Attack Wave %d' % (self.stage.value+1)
        self.stats.add(Herald(stage, pulse=True))
        self._wait_mouse_up()
        save_mouse = self.pointer.get_real_pos()
        while 1:
            if not self.song_is_playing():
                self.play_fx('marquis', True)
            e = pygame.event.poll()
            if e.type == MOUSEBUTTONUP:
                self.play_fx('click')
                pygame.event.get()
                jukebox['marquis'].fadeout()
                self.pointer.set_real_pos(save_mouse)
                break
            elif e.type == KEYDOWN:
                self._do_settings(e.key)
            self.update_bg()
            self.draw_bg(groups=[group])
            window.flip()
            self.fps.tick(self.max_fps)
        self._kill_heralds()
    def _sign_on(self):
        jukebox['computer greets'].play()
        window = self.window
        surface = window.surface
        sky = self.sky
        group = pygame.sprite.Group()
        group.add(Herald('Combat systems locked.', 0, False))
        group.add(Herald('Voice ident required...', 1, False))
        h4 = Herald('1) [%s] Coffee and music, ship!'%self._name, 3, False)
        group.add(h4)
        h5 = Herald('2) Change of command, ship.', 4, False)
        h5.rect.left = h4.rect.left
        group.add(h5)
        # Same player or Change player name
        if CODER_PASS:
            have_name = True
        else:
            have_name = False
        pygame.event.clear()
        while not have_name:
            self.play_fx('sign on', True)
            e = pygame.event.poll()
            if e.type == KEYUP:
                if e.key == K_1:
                    self.play_fx('click')
                    have_name = True
                    break
                elif e.key == K_2:
                    self.play_fx('click')
                    break
            self.update_bg(groups=[group], stats=False)
            self.draw_bg(groups=[group], stats=False)
            window.flip()
            self.fps.tick(self.max_fps)
        group.empty()
        # player name workspace and display
        if have_name:
            player_name = self._name
            player_edit = Herald(player_name, 1, False)
            player_edit.set_color(ui_color('stats'))
        else:
            group.add(
                Herald('Key in pilot handle for biometric ident...', 0, False))
            player_name = PLAYER_NAME_DEFAULT
            player_edit = Herald(player_name, 3, False)
            player_edit.set_color(ui_color('stats'))
            players = [player_edit]
            playersi = 0
            for o in self.high_scores.values():
                if o.name.isupper():
                    players.append(Herald(o.name, 1, False))
            players_display = players[playersi:playersi+2]
            for i in range(len(players_display)):
                players_display[i].set_line(i+3)
            group.add(*players_display)
        # get keys, update player name
        while not have_name:
            self.play_fx('sign on', True)
            e = pygame.event.poll()
            if e.type == KEYUP:
                self.play_fx('click')
                if e.key == K_DOWN:
                    # step down the player list
                    if playersi < len(players)-1:
                        if len(players_display) == 3:
                            h = players_display.pop(0)
                            h.kill()
                        players[playersi].set_color(ui_color('system'))
                        playersi += 1
                        players[playersi].set_color(ui_color('stats'))
                        try:
                            players_display.append(players[playersi+1])
                            group.add(players[playersi+1])
                        except IndexError:
                            pass
                        if playersi in (0,len(players_display)-1):
                            line = 3
                        else:
                            line = 2
                        for i in range(len(players_display)):
                            players_display[i].set_line(line+i)
                elif e.key == K_UP:
                    # step up the player list
                    if playersi > 0:
                        if len(players_display) == 3:
                            h = players_display.pop()
                            h.kill()
                        players[playersi].set_color(ui_color('system'))
                        playersi -= 1
                        players[playersi].set_color(ui_color('stats'))
                        if playersi > 0:
                            players_display.insert(0, players[playersi-1])
                            group.add(players[playersi-1])
                        if playersi == 0:
                            line = 3
                        else:
                            line = 2
                        for i in range(len(players_display)):
                            players_display[i].set_line(line+i)
                elif e.key > 255:
                    pass
                elif e.key == K_RETURN:
                    # process return
                    if player_name != PLAYER_NAME_DEFAULT:
                        player_name = player_name.rstrip(RUBOUT)
                        player_edit.set_text(player_name)
                        have_name = True
                elif players[playersi] is not player_edit:
                    pass
                elif e.key == K_BACKSPACE:
                    # process backspace
                    l = list(player_name)
                    i = player_name.find(RUBOUT)
                    if i > 0:
                        i -= 1
                    l[i] = RUBOUT
                    player_name = ''.join(l)
                    player_edit.text = player_name
                    player_edit.render_text()
                elif chr(e.key).isalnum():
                    # process letter
                    i = player_name.find(RUBOUT)
                    if i > -1:
                        l = list(player_name)
                        l[i] = chr(e.key).upper()
                        player_name = ''.join(l)
                        player_edit.set_text(player_name)
            self.update_bg(groups=[group], stats=False)
            self.draw_bg(groups=[group], stats=False)
            window.flip()
            self.fps.tick(self.max_fps)
        group.empty()
#        font = get_font('gui')
#        self.name = pygame.sprite.Sprite()
#        self.name.image = font.render(
#            player_name, True, ui_color('stats'))
#        self.name.rect = self.name.image.get_rect()
#        self.name.rect.topright = ui_layout().name
#        self.name.text = player_name
        self.name = PlayerName(player_name)
        self.stats.add(self.name)
        self._name = player_name
        jukebox['sign on'].fadeout()
        if not CODER_PASS:
            jukebox['power up'].play()
            self._segue(5, 2,
                'Identity confirmed, Commander %s.' % self._name,
                'Powering up combat systems.',
                'The ship is under your command.')
    def _post_high_scores(self):
        # This is not smart. Positions are hard-coded and depend on the
        # font size of the Herald class, and width of the Window class.
        surf = game.window.surface
        rect = game.window.rect
        pointer = self.pointer
        group = pygame.sprite.Group()
        line = -3
        group.add(Herald('Most Decorated Space Heroes', line))
        compare = lambda a,b: cmp(b.score, a.score)
        scores = sorted(self.high_scores.values(), compare)
        line += 2
        name = Herald('Name', line, False)
        stage = Herald('Stage', line, False)
        score = Herald('Commendations', line, False)
        name.rect.left = rect.left + 150
        score.rect.right = rect.right - 150
        stage.rect.centerx = rect.centerx + 15
        group.add(name, stage, score)
        line += 1
        for o in scores:
            if self.name.text == o.name and \
            self.stage.value+1 == o.stage and \
            self.score.value == o.score:
                pulse = True
            else:
                pulse = False
            name = Herald(o.name, line, pulse)
            stage = Herald(repr(o.stage), line, pulse)
            score = Herald(repr(o.score), line, pulse)
            name.rect.left = rect.left + 150
            score.rect.right = rect.right - 150
            stage.rect.right = rect.centerx + 25
            group.add(name, stage, score)
            line += 1
            if line > 7:
                break
        line += 1
        group.add(Herald('Acknowledge', line, True))
        self._wait_mouse_up()
        save_state = self.state
        self.set_state(HIGHSCORES)
        while 1:
            self.play_fx('high scores', True)
            e = pygame.event.poll()
            if e.type == MOUSEBUTTONUP:
                jukebox['high scores'].fadeout()
                self.play_fx('click')
                break
            self.update_bg(groups=[group], stats=False)
            self.draw_bg(groups=[group], stats=False)
            self.window.flip()
            self.fps.tick(self.max_fps)
        self.set_state(save_state)
    def _make_hud(self):
        # Should probably turn this into a class.
        fontw,fonth = get_font('gui_small').size('W')
        ## To add more to the hud...
        ## 1. Set this to the number of entries in dictionary self.hud.
        nr_stats = 11
        y = self.window.rect.bottom - (nr_stats+1)*fonth
        x = self.window.rect.right - 175
        ## 2. Add the entries(s) to the dict. Use Stat for static text,
        ##    Statf for a formatted text. If Stat, you are done.
        ## 3a. Add a statement to Game._update_hud (see step 3b).
        self.hud = {
            'MV' : Statf('Master volume (<>): %.2f', (x,y+fonth*0),
                        jukebox.get_master_volume()),
            'mv' : Statf('Music volume (-+): %.2f', (x,y+fonth*1),
                        self.music_volume),
            'fv' : Statf('Fx volume ([]): %.2f', (x,y+fonth*2),
                        self.fx_volume),
            'mp' : Statf('Music paused (M): %s', (x,y+fonth*3),
                        self.music_paused),
            'st' : Stat('Skip song track (T)', (x,y+fonth*4)),
            'sh' : Stat('Shuffle music (S)', (x,y+fonth*5)),
            'inv' : Statf('Player Invulnerable (I): %s', (x,y+fonth*6),
                        self._invulnerable),
            'prt' : Statf('Pointer visible (V) %s', (x,y+fonth*7),
                        self.pointer.visible),
            'gm' : Statf('Grab mouse (G): %s', (x,y+fonth*8),
                        self._grab_mouse),
            'tm' : Statf('Turn method (W) %s', (x,y+fonth*9),
                        self.player.turn_mode_name()),
#            'sa' : Statf('Ship angle %d', (x,y+fonth*10),
#                        self.player.angle),
#            'mx' : Statf('Mouse pos (%d,%d)', (x,y+fonth*10),
#                        pygame.mouse.get_pos()),
#            'px' : Statf('Pointer pos (%d,%d)', (x,y+fonth*11),
#                        self.pointer.rect.center),
#            'fm' : Statf('Fighter max patrol: %.1f', (x,y+fonth*10), 0.0),
#            'sm' : Statf('Sweeper max patrol: %s', (x,y+fonth*11), 0.0),
#            'km' : Statf('Kamikaze max patrol: %s', (x,y+fonth*12), 0.0),
            'hud' : Stat('Toggle this HUD (H)', (x,y+fonth*10)),
        }
        self.stats.add(self.hud.values())
        self._update_hud()
    def _toggle_hud(self):
        if self.show_hud:
            self.show_hud = False
            for s in self.hud.values():
                if s is not self.hud['hud']:
                    s.kill()
        else:
            self.show_hud = True
            for s in self.hud.values():
                if s is not self.hud['hud']:
                    self.stats.add(s)
    def _update_hud(self):
        if self.show_hud:
            hud = self.hud
            ## 3b. Add statements to update the values in dictionary Game.hud.
            ## 4. If user setting, add event code in Game._do_settings.
            hud['inv'].set_value(self._invulnerable)
            hud['prt'].set_value(self.pointer.visible)
            hud['gm'].set_value(self._grab_mouse)
            hud['MV'].set_value(jukebox.get_master_volume())
            hud['mv'].set_value(self.music_volume)
            hud['fv'].set_value(self.fx_volume)
            hud['mp'].set_value(self.music_paused)
            hud['tm'].set_value(self.player.turn_mode_name())
#            if self.enemy_factory is not None:
#                hud['fm'].set_value(
#                    self.enemy_factory.max_on_patrol[enemy.EnemyFighter])
#                hud['sm'].set_value(
#                    self.enemy_factory.max_on_patrol[enemy.EnemySweeper])
#                hud['km'].set_value(
#                    self.enemy_factory.max_on_patrol[enemy.EnemyKamikaze])
#            hud['sa'].set_value(self.player.angle)
#            hud['mx'].set_value(pygame.mouse.get_pos())
#            hud['px'].set_value(self.pointer.rect.center)
    def _make_debugs(self):
        if not DEBUG:
            return
        # Should probably turn this into a class.
        fontw,fonth = get_font('gui_small').size('W')
        y = self.window.rect.top + 10
        x = self.window.rect.right - 175
        self.debugs = {
            'debugs' : Stat('Toggle debug HUG (D)', (x,y+fonth*0)),
        }
        self.stats.add(self.debugs.values())
        self._update_debugs()
    def _toggle_debugs(self):
        if not DEBUG:
            return
        if self.show_debugs:
            self.show_debugs = False
            for s in self.debugs.values():
                if s is not self.debugs['debugs']:
                    s.kill()
        else:
            self.show_debugs = True
            for s in self.debugs.values():
                if s is not self.debugs['debugs']:
                    self.stats.add(s)
    def _update_debugs(self):
        if not DEBUG:
            return
        if self.show_debugs:
            debugs = self.debugs
    def _debug_it(self):
        if DEBUG:
            self.enemy_factory.draw_path()
            if self.pause:
                pygame.event.get()
                pygame.event.wait()
                self.pause = False

def main():
    global game, jukebox, music_tracks
    pygame.init()
    # Make the system-wide jukebox
    jukebox = gumm_sound.Jukebox()
    jukebox.set_master_volume(settings.master_volume)
    if CODER_PASS:
        # load subset of tracks to reduce loading time
        max_tracks = 0
        while len(music_tracks) < max_tracks:
            music_tracks.append(random.choice(os.listdir('music')))
            if len(music_tracks) == 2:
                if music_tracks[0] == music_tracks[1]:
                    music_tracks.pop()
    else:
        music_tracks.extend(os.listdir('music'))
    music_tracks.shuffle()
    # Make the game
    game_globals.game = game = Game()
    if False:
        import cProfile
#        cProfile.run('game.run()', 'run.prof')
        cProfile.run('game.run()')
    else:
        game.run()

if __name__ == '__main__':
    if USE_PSYCO:
        try:
            import psyco
            psyco.full()
            print 'psyco succeeded'
        except ImportError,e:
            print 'psyco failed:',e
    main()
